package generator

import (
	"fmt"
	"sort"
	"strings"
	"v8s-controller/internal/configuration"
	"v8s/compose/model"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	resource "k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type (
	DeploymentsGenerator struct {
		loadBalancers *LoadBalancerServiceGenerator
		services      *ServiceGenerator
		properties    configuration.GeneratorProperties
	}
)

const (
	deploymentPodSelectorLabel = "platform-v.drop-app/deployment"

	requestEphemeralStorage = "100Mi"
	limitEphemeralStorage   = "500Mi"
)

var (
	defaultReplicas int32 = 1
	zeroReplicas    int32 = 0

	DeploymentMeta = metav1.TypeMeta{
		Kind:       "Deployment",
		APIVersion: "apps/v1",
	}

	zeroQuantity = resource.Quantity{}
)

func NewDeploymentsGenerator(properties configuration.GeneratorProperties, loadBalancers *LoadBalancerServiceGenerator, services *ServiceGenerator) *DeploymentsGenerator {
	return &DeploymentsGenerator{
		loadBalancers: loadBalancers,
		services:      services,
		properties:    properties,
	}
}

func (g *DeploymentsGenerator) Generate(services model.Services, suspended bool, pvcs []*corev1.PersistentVolumeClaim, imagePullSecrets []*corev1.Secret, opts ...Option) ([]*appsv1.Deployment, error) {
	results := make([]*appsv1.Deployment, 0, len(services))

	for name, service := range services {
		generated, err := g.generate(name, service, suspended, pvcs, imagePullSecrets, opts...)
		if err != nil {
			return nil, fmt.Errorf("failed to generate deployment for service '%s': %w", name, err)
		}

		results = append(results, generated)
	}

	return results, nil
}

func (g *DeploymentsGenerator) generate(name model.ServiceName, service model.Service, suspended bool, pvcs []*corev1.PersistentVolumeClaim, imagePullSecrets []*corev1.Secret, opts ...Option) (*appsv1.Deployment, error) {
	deploymentSelectors := PodSelector{
		deploymentPodSelectorLabel: string(name),
	}
	serviceSelectors := g.services.GetSelector(name)
	lbSelectors := g.loadBalancers.GetSelector(service)

	selectors, err := appendPodSelectors(deploymentSelectors, serviceSelectors, lbSelectors)
	if err != nil {
		return nil, fmt.Errorf("failed to construct selector for service '%s': %w", name, err)
	}

	ports, err := g.generatePorts(name, service.Ports)
	if err != nil {
		return nil, fmt.Errorf("failed to generate ports: %w", err)
	}

	resources, err := g.generateResources(service.Deployment.Resources)
	if err != nil {
		return nil, fmt.Errorf("failed to generate reources: %w", err)
	}

	volumes, volumeMounts, resources, err := g.generateVolumes(name, service.Volumes, pvcs, resources)
	if err != nil {
		return nil, fmt.Errorf("failed to generate volumes: %w", err)
	}
	annotations := map[string]string{ResourceBelongsToAnnotation: string(name)}

	opt, exists := getOption[LoggingOption](opts...)
	if exists {
		annotations[LogPrefixAnnotation] = fmt.Sprintf("%s-%s", opt.LogPrefix, name) ///logPrefix
	}

	var runtimeClass *string
	if g.properties.Container.RuntimeClass != "" {
		runtimeClass = &g.properties.Container.RuntimeClass
	}

	spec := appsv1.DeploymentSpec{
		Selector: &metav1.LabelSelector{
			MatchLabels: deploymentSelectors,
		},
		Template: corev1.PodTemplateSpec{
			ObjectMeta: metav1.ObjectMeta{
				Annotations: annotations,
				Labels:      selectors,
			},
			Spec: corev1.PodSpec{
				RuntimeClassName: runtimeClass,
				Containers: []corev1.Container{
					{
						Name:         string(name),
						Image:        service.Image,
						Ports:        ports,
						Env:          g.generateEnvironments(service.Environment),
						Resources:    resources,
						VolumeMounts: volumeMounts,
					},
				},
				Volumes:          volumes,
				ImagePullSecrets: g.generateImagePullSecrets(imagePullSecrets),
			},
		},
		Replicas: &defaultReplicas,
	}

	if suspended {
		spec.Replicas = &zeroReplicas
	}

	return &appsv1.Deployment{
		TypeMeta: DeploymentMeta,
		ObjectMeta: metav1.ObjectMeta{
			Name:        string(name),
			Annotations: map[string]string{ResourceBelongsToAnnotation: string(name)},
		},
		Spec: spec,
	}, nil
}

func (g *DeploymentsGenerator) generateVolumes(name model.ServiceName, volumes model.VolumeMounts, claims []*corev1.PersistentVolumeClaim, resources corev1.ResourceRequirements) ([]corev1.Volume, []corev1.VolumeMount, corev1.ResourceRequirements, error) {
	podVolumes := sliceOrNil[corev1.Volume](len(volumes))
	podMounts := sliceOrNil[corev1.VolumeMount](len(volumes))

	for i, volume := range volumes {
		configuration, err := volume.ToVolumeConfiguration()
		if err != nil {
			return nil, nil, corev1.ResourceRequirements{}, fmt.Errorf("failed to parse volumes for service '%s': %w", name, err)
		}

		mount := corev1.VolumeMount{
			Name:      fmt.Sprintf("volume-%d", i),
			ReadOnly:  configuration.ReadOnly,
			MountPath: configuration.ContainerPath,
		}

		podMounts = append(podMounts, mount)

		if configuration.IsTemp && (!configuration.InMemory) {
			if resources.Limits == nil {
				resources.Limits = map[corev1.ResourceName]resource.Quantity{}
			}

			if resources.Requests == nil {
				resources.Requests = map[corev1.ResourceName]resource.Quantity{}
			}

			resources.Limits[corev1.ResourceEphemeralStorage] = resource.MustParse(limitEphemeralStorage)
			resources.Requests[corev1.ResourceEphemeralStorage] = resource.MustParse(requestEphemeralStorage)
		}

		podVolume, err := g.generatePodNamedVolume(mount.Name, configuration, claims)
		if err != nil {
			return nil, nil, corev1.ResourceRequirements{}, fmt.Errorf("failed to generate pod volume: %w", err)
		}

		podVolumes = append(podVolumes, podVolume)
	}

	return podVolumes, podMounts, resources, nil
}

func (g *DeploymentsGenerator) generateEnvironments(envs model.Environment) []corev1.EnvVar {
	if len(envs) == 0 {
		return nil
	}

	results := make([]corev1.EnvVar, 0, len(envs))

	for k, v := range envs {
		results = append(results, corev1.EnvVar{
			Name:  k,
			Value: v,
		})
	}

	sort.SliceStable(results, func(i, j int) bool {
		left := results[i]
		right := results[j]

		return strings.Compare(left.Name, right.Name) < 0
	})

	return results
}

func (g *DeploymentsGenerator) generatePorts(name model.ServiceName, ports model.Ports) ([]corev1.ContainerPort, error) {
	results := sliceOrNil[corev1.ContainerPort](len(ports))

	for _, port := range ports {
		portCfg, err := port.ToPortConfiguration()
		if err != nil {
			return nil, fmt.Errorf("failed to parse ports for service '%s': %w", name, err)
		}

		results = append(results, corev1.ContainerPort{
			Name:          portCfg.Identity(),
			ContainerPort: portCfg.ContainerPort,
		})
	}

	return results, nil
}

func findVolumeClaim(claims []*corev1.PersistentVolumeClaim, configuration model.VolumeConfiguration) (*corev1.PersistentVolumeClaim, error) {
	name := GenerateVolumeClaimName(configuration.VolumeSource)

	for _, claim := range claims {
		if claim.Name == name {
			return claim, nil
		}
	}

	return nil, fmt.Errorf("volume declaration matched to '%s' not found", name)
}

func (g *DeploymentsGenerator) generatePodNamedVolume(name string, configuration model.VolumeConfiguration, claims []*corev1.PersistentVolumeClaim) (corev1.Volume, error) {
	source := corev1.VolumeSource{}

	if configuration.IsTemp {
		source.EmptyDir = &corev1.EmptyDirVolumeSource{}

		if configuration.InMemory {
			source.EmptyDir.Medium = corev1.StorageMediumMemory
		}
	} else {
		claimName := GenerateVolumeClaimName(configuration.VolumeSource)

		exist := false
		for _, claim := range claims {
			if claim.Name == claimName {
				exist = true
			}
		}

		if !exist {
			return corev1.Volume{}, fmt.Errorf("claim '%s' not found for volume source '%s'", claimName, configuration.VolumeSource)
		}

		source.PersistentVolumeClaim = &corev1.PersistentVolumeClaimVolumeSource{
			ClaimName: claimName,
			ReadOnly:  configuration.ReadOnly,
		}
	}

	return corev1.Volume{
		Name:         name,
		VolumeSource: source,
	}, nil
}

func (g *DeploymentsGenerator) generateResources(resources model.Resources) (corev1.ResourceRequirements, error) {
	limits, err := mapModelResourceToResourceList(resources.Limits)
	if err != nil {
		return corev1.ResourceRequirements{}, fmt.Errorf("failed to generate limits: %w", err)
	}

	requests, err := mapModelResourceToResourceList(resources.Reservations)
	if err != nil {
		return corev1.ResourceRequirements{}, fmt.Errorf("failed to generate requests: %w", err)
	}

	if limits == nil && requests != nil {
		limits = make(corev1.ResourceList)
	}

	if limits[corev1.ResourceCPU] == zeroQuantity && requests[corev1.ResourceCPU] != zeroQuantity {
		limits[corev1.ResourceCPU] = requests[corev1.ResourceCPU]
	}

	if limits[corev1.ResourceMemory] == zeroQuantity && requests[corev1.ResourceMemory] != zeroQuantity {
		limits[corev1.ResourceMemory] = requests[corev1.ResourceMemory]
	}
	return corev1.ResourceRequirements{
		Limits:   limits,
		Requests: requests,
	}, nil
}

func (g *DeploymentsGenerator) generateImagePullSecrets(secrets []*corev1.Secret) []corev1.LocalObjectReference {
	results := sliceOrNil[corev1.LocalObjectReference](len(secrets))

	for _, secret := range secrets {
		current := secret

		results = append(results, corev1.LocalObjectReference{Name: current.Name})
	}

	return results
}

func mapModelResourceToResourceList(from model.Resource) (corev1.ResourceList, error) {
	cpu, err := from.Cpu.ToKubernetesCpu()
	if err != nil {
		return nil, fmt.Errorf("failed to convert cpu: %w", err)
	}

	ram := from.Memory.ToKubernetesRam()

	if cpu == 0 && ram == 0 {
		return nil, nil
	}

	list := corev1.ResourceList{}

	if cpu != 0 {
		list[corev1.ResourceCPU] = *resource.NewMilliQuantity(cpu, resource.DecimalSI)
	}

	if ram != 0 {
		list[corev1.ResourceMemory] = *resource.NewQuantity(ram, resource.DecimalSI)
	}

	return list, nil
}
